-module(db).
-export([startServers/1, stopServers/1, add/3, del/2, dump/1, getMax/1, getMin/1, flush/1, stop/1, initNeighbors/2,
print/1, getGossipMin/2, gMinFunction/4, setMinForEachProcess/1]).

% Exported helper functions.  Shouldn't be used by clients
-export([getMin_help/2, getMax_help/2, startServers_help/3, loop/1, processMsg/2, getMinFloatFromListSet/3, getMaxFloatFromListSet/3,
gMinFunction_helper/6, floor/1, ceiling/1]).

setMinForEachProcess([]) ->
	ok;
setMinForEachProcess(Servers) ->
	hd(Servers) ! {set_min_val},
	setMinForEachProcess(tl(Servers)).

% Gossip
getGossipMin([], TotalProcessNum) ->
	ok;
getGossipMin(Servers, TotalProcessNum) ->
	Ser = hd(Servers),
	Ser ! {gmin, TotalProcessNum, self()},
	%receive
	%	Msg -> io:format("~p~n", [Msg])
	%end, 
	%io:format("~nSending gmin to ~p,~nNumber Process: ~p~n", [Ser, TotalProcessNum]), 	
	getGossipMin(tl(Servers), TotalProcessNum).

gMinFunction(Id, NeighborNodes, TotalProcessNum, MYmin) ->
	TotalIter = ceiling(math:log(TotalProcessNum)),
	random:seed(now()),
	%io:format("~ngmin: ~p ~p ~p~n~p~n~p~n", [Id, MYmin, TotalProcessNum,NeighborNodes,TotalIter]),
	gMinFunction_helper(Id, MYmin, NeighborNodes, TotalProcessNum, 1, TotalIter).
	
gMinFunction_helper(Id, MYmin, NeighborNodes, TotalProcessNum, Created, TotalIter) when TotalIter+1 == Created ->
	MYmin;
gMinFunction_helper(Id, MYmin, NeighborNodes, TotalProcessNum, Created, TotalIter) ->
	%calculateNeighborProb(NeighborNodes),
	RandomNode = lists:nth(random:uniform(length(NeighborNodes)), NeighborNodes),
	%io:format("~n~p:~p is gossiping with ~p, sending ~p~n", [Created, Id, RandomNode, MYmin]),
	RandomNode ! {gmincomp, MYmin, self()},
	gMinFunction_helper(Id, processQueue(MYmin), NeighborNodes, TotalProcessNum, Created+1, TotalIter).	

calculateNeighborProb([]) ->
	ok;
calculateNeighborProb(NeighborNodes) ->
	hd(NeighborNodes) ! {sizeN, self()},
	receive
		Msg when is_integer(Msg) -> 
			io:format("Length ~p~n", [Msg])
	end,
	%Nnodes = popMessage(),
	%io:format("~p~n", [Nnodes]),
	calculateNeighborProb(tl(NeighborNodes)).	

popMessage() ->
	receive
		Msg -> Msg
	end.

processQueue(MYmin) ->
	receive
		Float when is_float(Float) ->
			if
				Float < MYmin ->
					Smin = Float;
				true ->
					Smin = MYmin
			end,
			processQueue(Smin)
	after
		100 -> MYmin
	end.

% starts multiple instances of a server
startServers(Num) ->
	startServers_help(Num, 1, []).

startServers_help(Num, Created, List) when Num == Created + 1 ->
	List;
startServers_help(Num, Created, List) ->
	Id = list_to_atom("server" ++ integer_to_list(Created)),
	Name = {Id, node()},
	%io:format("Name Dict: ~p~n", [Name]),
	start(Id),
	startServers_help(Num, Created + 1, [ Name | List ]).

% Starts a single instance of the server with the given Id
start(Id) ->
	Dict = dict:new(),
	NeighborNodes = [],
	FloatVal = 0.0,
	NeighborNodeNum = [],
	DataStruct = {Id, Dict, NeighborNodes, FloatVal, NeighborNodeNum},
	register(Id, spawn(db, loop, [DataStruct])),
	ok.
	 	
% Stops a list of servers
stopServers([]) ->
	ok;
stopServers(Servers) ->
	hd(Servers) ! quit,
	stopServers(tl(Servers)).

% Prints floats from each server
print([]) ->
	ok;
print(Servers) ->
	hd(Servers) ! {print, self()},
	print(tl(Servers)).

% initializes neighbor list of each server
initNeighbors([], Servers2) ->
	ok;
initNeighbors(Servers, Servers2) ->
	hd(Servers) ! {inineigh, Servers2, self()},
	receive
		Msg -> ok
	end,
	initNeighbors(tl(Servers), Servers2).

% Gets the minimum value from all the servers
getMin(Servers) ->
	getMin_help(Servers, 1.0E45).

getMin_help([], Min) ->
	Min;
getMin_help(Servers, Min) ->
	Ser = hd(Servers),
	MinServer = getMinFromServer(Ser),
	if
		MinServer < Min ->
			getMin_help(tl(Servers), MinServer);
		true ->
			getMin_help(tl(Servers), Min)
	end.

% Gets the maximum value from all the servers
getMax(Servers) ->
	getMax_help(Servers, -1.0E45).

getMax_help([], Max) ->
	Max;
getMax_help(Servers, Max) ->
	MaxServer = getMaxFromServer(hd(Servers)),
	if
		MaxServer > Max ->
			getMax_help(tl(Servers), MaxServer);
		true ->
			getMax_help(tl(Servers), Max)
	end.

% client functions to pass a message to the given server
add(Id, Tag, Float) ->
	Id ! {add, Tag, Float},
	ok.
	
del(Id, Tag) ->
	Id ! {del, Tag},
	ok.

getMaxFromServer(Id) ->
	Id ! {max, self()},
	receive
		Float when is_float(Float) ->
			Float
	end.

getMinFromServer(Id) ->
	Id ! {min, self()},
	receive
		Float when is_float(Float) ->
			Float
	end.

flush(Id) ->
	Id ! flush,
	ok.

dump(Id) ->
	Id ! {dump, self()},
	receive
		Dict ->
			Dict
	end.

stop(Id) ->
	Id ! quit,
	ok.

% Message loop for retrieving, processing, and stopping the server
loop(DataStruct) ->
	receive
		quit -> ok;
		flush ->
			loop(dict:new());
		Msg ->
			loop(processMsg(Msg, DataStruct))
	end.

processMsg(Msg, DataStruct) ->
	Id = element(1, DataStruct),
	Dict = element(2, DataStruct),
	NeighborNodes = element(3, DataStruct),
	Val = element(4, DataStruct),
	NeighborNodeNum = element(5, DataStruct),

	case Msg of
		{add, Tag, Float} when is_float(Float) ->
			io:format("~p\tAdding a float~p~n", [Id, Float]),	
			{Id, dict:append(Tag, Float, Dict), NeighborNodes, Val, NeighborNodeNum};
		{add, Tag, FloatList} when is_list(FloatList) ->
			case dict:is_key(Tag, Dict) of
				true ->
					io:format("~p\tKey ~p already exists", [Id, Tag]),
					DataStruct;
				_Other ->
					%io:format("~p\tAdding list of floats~n", [Id]),			
					{Id, dict:append_list(Tag, FloatList, Dict), NeighborNodes, Val, NeighborNodeNum}
			end;
		{addn, Node} when is_tuple(Node) ->
			io:format("~p\tAdding a neighbor node~p~n", [Id, Node]);	
		{del, Tag} ->
			io:format("~p Deleting elements tagged with ~p~n", [Id, Tag]),	
			dict:erase(Tag, Dict);
		{max, Proc} ->
			io:format("~p Getting maximum value~n", [Id]),	
			Proc ! getMaxFloatFromListSet(dict:fetch_keys(Dict), Dict, -1.0E45),
			DataStruct;
		{min, Proc} ->
			Smin = getMinFloatFromListSet(dict:fetch_keys(Dict), Dict, 1.0E45),
			io:format("~p's min value: ~p~n", [Id, Smin]),
			Proc ! Smin,
			DataStruct;
		{gmin, TotalProcessNum, Proc} ->
			%io:format("~ngmin: ~p ~p ~p~n~p~n", [Id, Val, TotalProcessNum,NeighborNodes]),
			Smin = gMinFunction(Id, NeighborNodes, TotalProcessNum, Val),
			%Proc ! "done",
			{Id, Dict, NeighborNodes, Smin, NeighborNodeNum};
		{gmincomp, NEWmin, Proc} ->
			%io:format("~nMin ~p's min ~p:, received min:~p~n", [Id, Val, NEWmin]),
			if
				Val < NEWmin ->
					Smin = Val;
				true ->
					Smin = NEWmin
			end,
			Proc ! Smin,
			{Id, Dict, NeighborNodes, Smin, NeighborNodeNum};
		{set_min_val} ->
			Smin = getMinFloatFromListSet(dict:fetch_keys(Dict), Dict, 10000.0),
			%io:format("~n~p's Min Val: ~p~n", [Id, Smin]),
			{Id, Dict, NeighborNodes, Smin, NeighborNodeNum};
		{inineigh, Servers2, Proc} ->
			Name = {Id, node()},
			Slist = lists:delete(Name, Servers2),
			calculateNeighborProb(Slist),
			Proc ! "done",
			%io:format("inineigh: ~p's Val: ~p, Neighbors: ~p~n", [Id, Val, Slist]),
			{Id, Dict, Slist, Val, NeighborNodeNum};
		{print, Proc} ->
			io:format("~n~p's Min Val: ~p~n", [Id, Val]),
			%io:format("~p's Val: ~p, Neighbors: ~p~n", [Id, Val, NeighborNodes]),
			DataStruct;
		{sizeN, Proc} ->
			%io:format("~p~n", [length(NeighborNodes)]),
			Proc ! length(NeighborNodes),
			DataStruct;
		{dump, Proc} ->
			Proc ! Dict,
			DataStruct
	end.

getMinFloatFromListSet([], _Dict, Min) ->
    Min;
getMinFloatFromListSet(Keys, Dict, Min) ->
    Key = hd(Keys),
    Values = dict:fetch(Key, Dict),
    ListMin = lists:min(Values),
    if
        ListMin < Min ->
            getMinFloatFromListSet(tl(Keys), Dict, ListMin);
        true ->
            getMinFloatFromListSet(tl(Keys), Dict, Min)
    end.
    
getMaxFloatFromListSet([], _Dict, Max) ->
    Max;
getMaxFloatFromListSet(Keys, Dict, Max) ->
    Key = hd(Keys),
    Values = dict:fetch(Key, Dict),
    ListMax = lists:max(Values),
    if
        ListMax > Max ->
            getMaxFloatFromListSet(tl(Keys), Dict, ListMax);
        true ->
            getMaxFloatFromListSet(tl(Keys), Dict, Max)
    end.

% floor and ceiling functions
floor(X) ->
    T = erlang:trunc(X),
    case (X - T) of
        Neg when Neg < 0 -> T - 1;
        Pos when Pos > 0 -> T;
        _ -> T
    end.

ceiling(X) ->
    T = erlang:trunc(X),
    case (X - T) of
        Neg when Neg < 0 -> T;
        Pos when Pos > 0 -> T + 1;
        _ -> T
    end.

